Validator Modules
Overview
Validator modules (IValidator) handle transaction validation and signature verification in ERC-7579 smart accounts. They implement both UserOp validation for ERC-4337 and signature validation for ERC-1271.
Interface Definition
interface IValidator is IModule {
error InvalidTargetAddress(address target);
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) external returns (uint256);
function isValidSignatureWithSender(
address sender,
bytes32 hash,
bytes calldata data
) external view returns (bytes4);
}
Implementation Example: ECDSA Validator
contract ECDSAValidator is IValidator {
using ECDSA for bytes32;
using MessageHashUtils for bytes32;
bytes4 private constant _ERC1271_MAGIC = 0x1626ba7e;
mapping(address => bool) private initializedAccounts;
// Core Module Functions
function onInstall(bytes calldata) external override {
if (isInitialized(msg.sender)) revert AlreadyInitialized(msg.sender);
initializedAccounts[msg.sender] = true;
}
function onUninstall(bytes calldata) external override {
if (!isInitialized(msg.sender)) revert NotInitialized(msg.sender);
initializedAccounts[msg.sender] = false;
}
function isModuleType(uint256 moduleTypeId) external pure override returns (bool) {
return moduleTypeId == MODULE_TYPE_VALIDATOR;
}
function isInitialized(address smartAccount) public view override returns (bool) {
return initializedAccounts[smartAccount];
}
// Validator-specific Functions
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) external view override returns (uint256 validationData) {
bytes calldata signature = userOp.signature;
bytes32 r = bytes32(signature[0x00:0x20]);
bytes32 s = bytes32(signature[0x20:0x40]);
uint8 v = uint8(bytes1(signature[0x40:0x41]));
(address recoveredAddr, ECDSA.RecoverError error, ) = ECDSA.tryRecover(
userOpHash.toEthSignedMessageHash(),
v,
r,
s
);
return _isOwner(msg.sender, recoveredAddr)
? SIG_VALIDATION_SUCCESS
: SIG_VALIDATION_FAILED;
}
function isValidSignatureWithSender(
address,
bytes32 hash,
bytes calldata data
) external view returns (bytes4) {
address owner = msg.sender;
return SignatureCheckerLib.isValidSignatureNowCalldata(owner, hash, data)
? _ERC1271_MAGIC
: bytes4(0);
}
function _isOwner(
address smartAccount,
address addr
) private view returns (bool) {
bytes memory callData = abi.encodeWithSelector(
IOwnerManager.k1IsOwner.selector,
addr
);
// Assembly implementation for gas optimization
assembly {
let result := staticcall(
gas(),
smartAccount,
add(callData, 0x20),
mload(callData),
0x00,
0x20
)
if result {
isOwner := mload(0x00)
}
}
}
}
Key Features
-
UserOp Validation
- Validates ERC-4337 user operations
- Handles signature verification
- Integrates with account ownership system
-
ERC-1271 Support
- Contract signature validation
- Standard signature interface
- Multiple signature schemes
-
Security Features
- Signature replay protection
- Owner validation
- Gas optimization
Common Use Cases
-
Multi-Signature Validation
- Multiple owner validation
- Threshold signatures
- Time-locked operations
-
Session Key Management
- Time-limited permissions
- Scoped permissions
- Gas payment delegation
-
Custom Authentication
- Hardware wallet integration
- Social recovery
- MPC signatures
Security Considerations
-
Signature Verification
- Use standard libraries (OpenZeppelin, Solady)
- Validate signature formats
- Handle malformed signatures
-
Access Control
- Owner validation
- Permission checks
- Initialization status
-
Gas Optimization
- Efficient signature verification
- Minimal storage usage
- Assembly optimizations when appropriate
Implementation Guidelines
- Basic Structure
contract CustomValidator is IValidator {
// Track initialization status
mapping(address => bool) private initializedAccounts;
// Core module functions
function onInstall(bytes calldata data) external override {
// Installation logic
}
function onUninstall(bytes calldata data) external override {
// Cleanup logic
}
// Validator-specific functions
function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) external returns (uint256) {
// Validation logic
}
function isValidSignatureWithSender(
address sender,
bytes32 hash,
bytes calldata data
) external view returns (bytes4) {
// Signature validation logic
}
}
-
Testing Requirements
- UserOp validation tests
- Signature verification tests
- Access control tests
- Gas optimization tests
-
Integration Testing
- Smart account interaction
- ERC-4337 bundler compatibility
- Gas estimation accuracy
Best Practices
-
Error Handling
- Use custom errors
- Descriptive error messages
- Proper validation checks
-
Gas Optimization
- Use assembly for repetitive operations
- Minimize storage operations
- Batch operations when possible
-
Security
- Implement replay protection
- Validate all inputs
- Use safe math operations